// TODO: CODE REVIEW & CLEANUP!
//
// SunfireDriver.cs
//
// Implements HomeUX.Components.SunfireDriver.
//
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Xml;
using HomeUX.DriverModel;
using HomeUX.Networking;
using HomeUX.Synchronization;
using HomeUX.Utilities;

namespace HomeUX.Components
{

    /* TODO: format:
 
      Single channel inputs and outputs; an output can be connected to zero, one, or two inputs
      (the latter example is for mixing left and right for a mono zone).  SyncPath schema:
        -- "<device>\IsPowered" = (true or false)
        -- "<device>\Volume" = volume (0.0 to 1.0)
        -- "<device>\IsMuted" = mute value (true or false)
        -- "<device>\Source" = name of current audio source
        -- "<device>\Effect" = name of current audio effect
        -- "Configuration\<device>\SourceNames\<name>" = one for each source; <name> is the source name,
           the node value is the display label for the source to use (may be the same as the name)
        -- "Configuration\<device>\EffectNames\<name>" = one for each audio effect; <name> is the effect name,
           the node value is the display label for the effect to use (may be the same as the name)
    */
    /// <summary>
/// Implements a HomeUX driver for the Sunfire Theater Grand Processor II.
/// </summary>
///
public class SunfireDriver : Driver
{
    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Private Fields
    //

    /// <summary>
    /// Maps the name of each source specified in the .config file to the corresponding
    /// <r>SunfireInputId</r>.  Unused inputs are not included in this dictionary.
    /// </summary>
    Dictionary<string, SunfireInputId> _mapSourceNameToInputId;

    /// <summary>
    /// The current "IsPowered" setting.
    /// </summary>
    bool _isPowered;

    /// <summary>
    /// The current volume level as specified by clients, in the range 0.0 to 1.0.  This level
    /// is not bound or quantized; for example, values like 0.1234567 or -42 are legal, even
    /// though, for this particular hardware, they're equivalent to 0.12 and 0, respectively.
    /// </summary>
    double _volume;

	/// <summary>
	/// The current "IsMuted" setting.
	/// </summary>
	bool _isMuted;

    /// <summary>
    /// The current "Source" setting.
    /// </summary>
    string _sourceName;

    /// <summary>
    /// The current audio effect name setting.
    /// </summary>
    string _effectName;

    /// <summary>
    /// The current "IsHolo" setting.
    /// </summary>
    bool _isHolo;

    /// <summary>
    /// The serial port specification string that defines the virtual or real serial port to open.
    /// </summary>
    string _serialPortSpec;

    /// <summary>
    /// Provides device-specific hardware access.
    /// </summary>
    SunfireHardware _hardware;

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Device Class Overrides
    //

    protected override void OnInitialize()
    {
        // read attributes of the "<Device>" XML element
        _serialPortSpec = Configuration.GetRequiredAttribute("SerialPort");

        // read attributes of child XML element
        _mapSourceNameToInputId = new Dictionary<string, SunfireInputId>(20);
        foreach (ConfigurationElement element in Configuration.Children)
        {
            switch (element.Name)
            {

            case "Source":

                string sourceName = element.GetRequiredAttribute("Name");

                SunfireInputId inputId;
                try
                {
                    inputId = SunfireHardware.ParseInputId(
                        element.GetRequiredAttribute("InputId"));
                }
                catch (ArgumentException ex)
                {
                    throw new ConfigurationException(element, ex.Message);
                }

                try
                {
                    _mapSourceNameToInputId.Add(sourceName, inputId);
                }
                catch (ArgumentException)
                {
                    throw new ConfigurationException(element,
						"Duplicate source name: {0}", sourceName);
                }

                // if we haven't assigned <_sourceName> yet, give an error
                if (_sourceName == null)
                    _sourceName = sourceName;
                break;

            }
        }

        // load previously-persisted state
        _isPowered = PersistedTree.TryFindValue("IsPowered", false);
        _volume = PersistedTree.TryFindValue("Volume", 0.0);
        _isMuted = PersistedTree.TryFindValue("IsMuted", false);
        _sourceName = PersistedTree.TryFindValue("Source", _sourceName);
        _effectName = PersistedTree.TryFindValue("Effect",
            SunfireHardware.FormatEffectId(SunfireEffectId.Stereo));
        _isHolo = PersistedTree.TryFindValue("IsHolo", false);
    }

    protected override void OnEnable()
    {
        QueueRequest(new SunfireEnableRequest() { Driver = this }, false);
    }

    protected override void OnDisable()
    {
        CloseHardware();
        DisableCompleted(); // disable completed synchronously
    }

    // can throw VirtualSerialPortException on error
    void OpenHardware()
    {
        VirtualSerialPort serialPort = null;
        try
        {
            serialPort = VirtualSerialPort.GetPort(_serialPortSpec);
            _hardware = new SunfireHardware(WorkQueue, serialPort, Name);

            // IMPORTANT: do corresponding "-=" in CloseHardware
            _hardware.CommandTimedOut += OnHardwareCommandTimedOut;
            _hardware.WarningsReported += OnHardwareWarningsReported;
            // IMPORTANT: do corresponding "-=" in CloseHardware

            _hardware.Open();
        }
        finally
        {
            // if <serialPort> was allocated but ownership of it wasn't transferred to
            // <_hardware>, free <serialPort> to prevent a memory leak
            if ((serialPort != null) && (_hardware == null))
            {
                serialPort.Dispose();
                serialPort = null;
            }
        }

    }

    void CloseHardware()
    {
        _hardware.CommandTimedOut -= OnHardwareCommandTimedOut;
        _hardware.WarningsReported -= OnHardwareWarningsReported;
        _hardware.Dispose();
        _hardware = null;
    }

    public override void SerializeStateForClient(Guid sessionId, XmlWriter writer)
    {
        SerializeSyncNodeUpdate(writer, _isPowered, Name, "IsPowered");
        SerializeSyncNodeUpdate(writer, _volume, Name, "Volume");
        SerializeSyncNodeUpdate(writer, _isMuted, Name, "IsMuted");
        SerializeSyncNodeUpdate(writer, _sourceName, Name, "Source");
        SerializeSyncNodeUpdate(writer, _effectName, Name, "Effect");
        SerializeSyncNodeUpdate(writer, _isHolo, Name, "IsHolo");

        SyncPath sourceNamesPath = SyncPath.Build("Configuration", Name, "SourceNames");
        foreach (string sourceName in _mapSourceNameToInputId.Keys)
            SerializeSyncNodeUpdate(writer, sourceName, sourceNamesPath, sourceName);

        SyncPath effectNamesPath = SyncPath.Build("Configuration", Name, "EffectNames");
        foreach (string effectName in SunfireHardware.EffectNames)
            SerializeSyncNodeUpdate(writer, effectName, effectNamesPath, effectName);
    }

    public override void ProcessSyncNodeUpdate(Guid originator, SyncNodeUpdate update,
        MessageBuilder replyBuilder)
    {
        update.Path.VerifyPathLength(2);
        switch (update.Path.MatchRequiredPart(1, "IsPowered", "Volume", "IsMuted", "Source", "Effect", "IsHolo"))
        {

        // the order of these cases is the same as the order of strings in MatchRequiredPart above

        case 0: // "IsPowered"

            QueueRequest(new SunfireIsPoweredChangeRequest()
            {
                Driver = this,
                Operation = update,
                Originators = new List<Guid>() { originator },
                NewIsPowered = update.GetTypedValue<bool>()
            }, true);
            break;

        case 1: // "Volume"

            QueueRequest(new SunfireVolumeChangeRequest()
            {
                Driver = this,
                Operation = update,
                Originators = new List<Guid>() { originator },
                NewVolume = update.GetTypedValue<double>()
            }, true);
            break;

        case 2: // "IsMuted"

            QueueRequest(new SunfireIsMutedChangeRequest()
            {
                Driver = this,
                Operation = update,
                Originators = new List<Guid>() { originator },
                NewIsMuted = update.GetTypedValue<bool>()
            }, true);
            break;

        case 3: // "Source"

            string newSourceName = update.GetTypedValue<string>();
            QueueRequest(new SunfireSetSourceRequest()
            {
                Driver = this,
                Operation = update,
                Originators = new List<Guid>() { originator },
                NewSourceName = newSourceName,
                NewInputId = ParseSourceName(newSourceName, update)
            }, true);
            break;

        case 4: // "Effect"

            string newEffectName = update.GetTypedValue<string>();
            QueueRequest(new SunfireSetEffectRequest()
            {
                Driver = this,
                Operation = update,
                Originators = new List<Guid>() { originator },
                NewEffectName = newEffectName,
                NewEffectId = ParseEffectName(newEffectName, update)
            }, true);
            break;

        case 5: // "IsHolo"

            QueueRequest(new SunfireIsHoloChangeRequest()
            {
                Driver = this,
                Operation = update,
                Originators = new List<Guid>() { originator },
                NewIsHolo = update.GetTypedValue<bool>()
            }, true);
            break;
        }
    }

	///////////////////////////////////////////////////////////////////////////////////////////////
	// IDisposable Overrides
	//

    public override void Dispose()
    {
        if (_hardware != null)
        {
            _hardware.Dispose();
            _hardware = null;
        }
        base.Dispose();
    }

	///////////////////////////////////////////////////////////////////////////////////////////////
	// Helper Methods
	//

    protected override void MergeRequests(IList<Driver.Request> futureRequests)
    {
        // merge all SunfireVolumeChangeRequest objects that are adjacent at the beginning of <_futureRequests>

        while (true)
        {
            if (futureRequests.Count < 2)
                return;

            var first = futureRequests[0] as SunfireVolumeChangeRequest;
            if (first == null)
                return;

            var next = futureRequests[1] as SunfireVolumeChangeRequest;
            if (next == null)
                return;

            next.Originators.AddRange(first.Originators);
            futureRequests.RemoveAt(0);
        }
    }

    /// <summary>
    /// Parses a source name, and returns the corresponding <r>SunfireInputId</r>.
    /// </summary>
    ///
    /// <param name="sourceName">The input name.</param>
    ///
    /// <param name="update">The <r>SyncNodeUpdate</r> that contained
    ///     <pr>connectionSpec</pr>.  Used for error reporting purposes.</param>
    ///
    /// <exception cref="SyncException">
    /// <pr>sourceName</pr> is invalid.
    /// </exception>
    ///
	SunfireInputId ParseSourceName(string sourceName, SyncNodeUpdate update)
	{
        SunfireInputId inputId;
		if (_mapSourceNameToInputId.TryGetValue(sourceName, out inputId))
			return inputId;
		else
			throw new SyncException(update, "Source \"{0}\" not found", sourceName);
	}

    /// <summary>
    /// Parses an audio effect name, and returns the corresponding <r>SunfireEffectId</r>.
    /// </summary>
    ///
    /// <param name="effectName">The effect name.</param>
    ///
    /// <param name="update">The <r>SyncNodeUpdate</r> that contained
    ///     <pr>connectionSpec</pr>.  Used for error reporting purposes.</param>
    ///
    /// <exception cref="SyncException">
    /// <pr>effectName</pr> is invalid.
    /// </exception>
    ///
    SunfireEffectId ParseEffectName(string effectName, SyncNodeUpdate update)
    {
        try
        {
            return SunfireHardware.ParseEffectId(effectName);
        }
        catch (ArgumentException ex)
        {
            throw new SyncException(update, ex.Message);
        }
    }

    static int DriverVolumeToHardwareVolume(double driverVolume)
    {
        return (int)Math.Round(driverVolume * 100);
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    //

    public void OnHardwareCommandTimedOut(object sender, SimpleSerialHardwareCommandTimedOutEventArgs e)
    {
        ReportAlert(MessageAlertType.Warning, "\"{0}\" command timed out", e.Command);
    }

    public void OnHardwareWarningsReported(object sender, SunfireHardwareWarningsReportedEventArgs e)
    {
        foreach (string message in e.Messages)
            ReportAlert(MessageAlertType.Warning, message);
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // SunfireRequest and Derived Types
    //

    abstract class SunfireRequest : Driver.Request
    {
        protected new SunfireDriver Driver
        {
            [DebuggerStepThrough]
            get
            {
                return (SunfireDriver)base.Driver;
            }
        }
    }

    [DebuggerDisplay("Enable driver")]
    class SunfireEnableRequest : SunfireRequest, Driver.IEnableRequest
    {
        protected override IEnumerable<bool> Implementation()
        {
            // open the hardware
            try
            {
                Driver.OpenHardware();
            }
            catch (VirtualSerialPortException ex)
            {
                Error = Util.FormatExceptionMessage(ex);
                yield break; // end JdsRequest
            }

            // invoke SunfireIsPoweredChangeRequest as a nested request
            var request = new SunfireIsPoweredChangeRequest()
            {
                Driver = Driver,
                NewIsPowered = Driver._isPowered,
                OverrideOnExit = NextState
            };
            request.Start();
            yield return true;
            if (request.Error != null)
            {
                Error = request.Error;
                yield break;
            }
        }
    }

    [DebuggerDisplay("{DebuggerDisplay}")]
    class SunfireIsPoweredChangeRequest : SunfireRequest
    {
        public bool NewIsPowered;

        protected override IEnumerable<bool> Implementation()
        {
            // power the hardware on or off
            if (NewIsPowered)
            {
                // turn on the hardware
                var setIsPowered = new ResultHolder<GenericReply>(this);
				Driver.Trace(true, "HW: Power On");
                Driver._hardware.SetIsPowered(true, setIsPowered.OnDone);
                yield return true; // wait for async reply
                if (setIsPowered.Result == null)
                {
                    Error = "SetIsPowered failed";
                    yield break;
                }
                else
                    Driver._isPowered = true;

                // set the mute setting
                var setIsMuted = new ResultHolder<GenericReply>(this);
				Driver.Trace(true, "HW: Set IsMuted={0} (due to Power On)", Driver._isMuted);
                Driver._hardware.SetIsMuted(Driver._isMuted, setIsMuted.OnDone);
                yield return true; // wait for async reply
                if (setIsMuted.Result == null)
                {
                    Error = "SetIsMuted failed during power-on";
                    yield break;
                }

                // set the volume, if we're not muted
                if (!Driver._isMuted)
                {
                    var setVolume = new ResultHolder<GenericReply>(this);
                    var hardwareVolume = DriverVolumeToHardwareVolume(Driver._volume);
					Driver.Trace(true, "HW: Set Volume={0} ({1}) (due to Power On)",
						hardwareVolume, Driver._volume);
                    Driver._hardware.SetVolume(hardwareVolume, setVolume.OnDone);
                    yield return true; // wait for async reply
                    if (setVolume.Result == null)
                    {
                        Error = "SetVolume failed during power-on";
                        yield break;
                    }
                }

                // set the source
                SunfireInputId inputId = Driver.ParseSourceName(Driver._sourceName, null);
                var setSource = new ResultHolder<GenericReply>(this);
				Driver.Trace(true, "HW: Set Source={0} ({1}) (due to Power On)",
					inputId, Driver._sourceName);
                Driver._hardware.SetSource(inputId, setSource.OnDone);
                yield return true;
                if (setSource.Result == null)
                {
                    Error = "SetSource failed during power-on";
                    yield break;
                }

                // set the effect setting
                SunfireEffectId effectId = Driver.ParseEffectName(Driver._effectName, null);
                var setEffect = new ResultHolder<GenericReply>(this);
				Driver.Trace(true, "HW: Set Effect={0} (due to Power On)", effectId);
                Driver._hardware.SetEffect(effectId, setEffect.OnDone);
                yield return true;
                if (setEffect.Result == null)
                {
                    Error = "SetEffect failed during power-on";
                    yield break;
                }
            }
            else
            {
                // turn off the hardware
                var setIsPowered = new ResultHolder<GenericReply>(this);
				Driver.Trace(true, "HW: Power Off");
                Driver._hardware.SetIsPowered(false, setIsPowered.OnDone);
                yield return true; // wait for async reply
                if (setIsPowered.Result == null)
                {
                    Error = "SetIsPowered failed";
                    yield break;
                }
                else
                    Driver._isPowered = false;
            }
        }
	}

    [DebuggerDisplay("{DebuggerDisplay}")]
	class SunfireVolumeChangeRequest : SunfireRequest
	{
		public double NewVolume;

        protected override IEnumerable<bool> Implementation()
        {
            // set the hardware volume level to <NewVolume>; if the hardware is off or the output
            // is muted, there's nothing to tell the hardware at this point -- we'll use
            // <NewVolume> when the output is unmuted
            if (Driver._isPowered && !Driver._isMuted)
            {
                var setVolume = new ResultHolder<GenericReply>(this);
                var hardwareVolume = DriverVolumeToHardwareVolume(NewVolume);
				Driver.Trace(true, "HW: Set Volume={0} ({1})", hardwareVolume, NewVolume);
                Driver._hardware.SetVolume(hardwareVolume, setVolume.OnDone);
                yield return true; // wait for async reply
                if (setVolume.Result == null)
                {
                    Error = "SetVolume failed";
                    yield break;
                }
            }

            // update driver state
            Driver._volume = NewVolume;
        }

        /// <summary>
        /// Returns a value used by <c>[DebuggerDisplay]</c>, encapsulated to prevent the debugger
        /// from displaying it in quotes.
        /// </summary>
        public object DebuggerDisplay
        {
            get
            {
                return new EncapsulatedString("Change Volume to {0}", NewVolume);
            }
        }
    }

    [DebuggerDisplay("{DebuggerDisplay}")]
    class SunfireIsMutedChangeRequest : SunfireRequest
    {
        public bool NewIsMuted;

        protected override IEnumerable<bool> Implementation()
        {
            // if the hardware is off, we'll just update internal state (below)
            if (Driver._isPowered)
            {
                // mute or unmute the hardware
                if (NewIsMuted)
                {
                    var setIsMuted = new ResultHolder<GenericReply>(this);
					Driver.Trace(true, "HW: Mute");
                    Driver._hardware.SetIsMuted(true, setIsMuted.OnDone);
                    yield return true; // wait for async reply
                    if (setIsMuted.Result == null)
                    {
                        Error = "SetIsMuted failed";
                        yield break;
                    }
                }
                else
                {
                    // to un-mute, we set the volume rather than using SetIsMuted(false), since this
                    // is slightly more reliable -- it works even in the case where the hardware was
                    // power cycled since the last call to SetVolume
                    var setVolume = new ResultHolder<GenericReply>(this);
                    var hardwareVolume = DriverVolumeToHardwareVolume(Driver._volume);
					Driver.Trace(true, "HW: Set Volume={0} ({1}) to un-mute", hardwareVolume,
						Driver._volume);
                    Driver._hardware.SetVolume(hardwareVolume, setVolume.OnDone);
                    yield return true; // wait for async reply
                    if (setVolume.Result == null)
                    {
                        Error = "SetVolume failed";
                        yield break;
                    }
                }
            }

            // update driver state
            Driver._isMuted = NewIsMuted;
        }

        /// <summary>
        /// Returns a value used by <c>[DebuggerDisplay]</c>, encapsulated to prevent the debugger
        /// from displaying it in quotes.
        /// </summary>
        public object DebuggerDisplay
        {
            get
            {
                return new EncapsulatedString("Change IsMuted to {0}", NewIsMuted);
            }
        }
    }

    [DebuggerDisplay("{DebuggerDisplay}")]
    class SunfireSetSourceRequest : SunfireRequest
	{
		public string NewSourceName;
        public SunfireInputId NewInputId;

        protected override IEnumerable<bool> Implementation()
        {
            // if the hardware is off, we'll just update internal state (below)
            if (Driver._isPowered)
            {
                var setSource = new ResultHolder<GenericReply>(this);
				Driver.Trace(true, "HW: Set Source={0} ({1})", NewInputId, NewSourceName);
                Driver._hardware.SetSource(NewInputId, setSource.OnDone);
                yield return true;
                if (setSource.Result == null)
                {
                    Error = "SetSource failed";
                    yield break;
                }
            }

            // update driver state
            Driver._sourceName = NewSourceName;
        }

        /// <summary>
        /// Returns a value used by <c>[DebuggerDisplay]</c>, encapsulated to prevent the debugger
        /// from displaying it in quotes.
        /// </summary>
        public object DebuggerDisplay
        {
            get
            {
				return new EncapsulatedString("Set Source to \"{0}\" ({1})", NewSourceName,
					Driver._mapSourceNameToInputId[NewSourceName]);
            }
        }
    }

    [DebuggerDisplay("{DebuggerDisplay}")]
    class SunfireSetEffectRequest : SunfireRequest
	{
		public string NewEffectName;
        public SunfireEffectId NewEffectId;

        protected override IEnumerable<bool> Implementation()
        {
            // if the hardware is off, we'll just update internal state (below)
            if (Driver._isPowered)
            {
                var setEffect = new ResultHolder<GenericReply>(this);
				Driver.Trace(true, "HW: Set Effect={0}", NewEffectId);
                Driver._hardware.SetEffect(NewEffectId, setEffect.OnDone);
                yield return true;
                if (setEffect.Result == null)
                {
                    Error = "SetEffect failed";
                    yield break;
                }
            }

            // update driver state
            Driver._effectName = NewEffectName;
        }

        /// <summary>
        /// Returns a value used by <c>[DebuggerDisplay]</c>, encapsulated to prevent the debugger
        /// from displaying it in quotes.
        /// </summary>
        public object DebuggerDisplay
        {
            get
            {
				return new EncapsulatedString("Set Effect to \"{0}\"", NewEffectName);
            }
        }
    }

    [DebuggerDisplay("{DebuggerDisplay}")]
    class SunfireIsHoloChangeRequest : SunfireRequest
    {
        public bool NewIsHolo;

        protected override IEnumerable<bool> Implementation()
        {
            // if the hardware is off, we'll just update internal state (below)
            if (Driver._isPowered)
            {
				var setIsHolo = new ResultHolder<GenericReply>(this);
				Driver.Trace(true, "HW: Set IsHolo={0}", NewIsHolo);
				Driver._hardware.SetIsHolo(NewIsHolo, setIsHolo.OnDone);
				yield return true; // wait for async reply
				if (setIsHolo.Result == null)
				{
					Error = "SetIsHolo failed";
					yield break;
				}
            }

            // update driver state
            Driver._isHolo = NewIsHolo;
        }

        /// <summary>
        /// Returns a value used by <c>[DebuggerDisplay]</c>, encapsulated to prevent the debugger
        /// from displaying it in quotes.
        /// </summary>
        public object DebuggerDisplay
        {
            get
            {
                return new EncapsulatedString("Change IsHolo to {0}", NewIsHolo);
            }
        }
    }
}

}
